# obsapi.py
import re
import cv2
import numpy as np
import base64
import pyautogui  
import time
import win32gui
import win32con  
import win32process  
import win32api  
import psutil
import subprocess  
import ctypes  
from ctypes import wintypes  
import pydirectinput

class OBSApi:
    def __init__(self):
        self.small_image_cache = {} #本地图片缓存
        self.vk_codes = {  
            'a': 0x41, 'b': 0x42, 'c': 0x43, 'd': 0x44,  
            'e': 0x45, 'f': 0x46, 'g': 0x47, 'h': 0x48,  
            'i': 0x49, 'j': 0x4A, 'k': 0x4B, 'l': 0x4C,  
            'm': 0x4D, 'n': 0x4E, 'o': 0x4F, 'p': 0x50,  
            'q': 0x51, 'r': 0x52, 's': 0x53, 't': 0x54,  
            'u': 0x55, 'v': 0x56, 'w': 0x57, 'x': 0x58,  
            'y': 0x59, 'z': 0x5A,  
            '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33,  
            '4': 0x34, '5': 0x35, '6': 0x36, '7': 0x37,  
            '8': 0x38, '9': 0x39,  
            ' ': 0x20,  # 空格键  
            '-': 0xBD,  # 减号/下划线  
            '=': 0xBB,  # 等号  
            '[': 0xDB,  # 左方括号  
            ']': 0xDD,  # 右方括号  
            '\\': 0xDC, # 反斜杠  
            ';': 0xBA,  # 分号  
            '\'': 0xDE, # 单引号  
            ',': 0xBC,  # 逗号  
            '.': 0xBE,  # 句号  
            '/': 0xBF,  # 斜杠  
            'shift': 0x10
            # ... 可以继续添加其他字符的映射  
        }

    # 获取桌面画面
    # 返回值：opencv的image对象
    def get_screenshot_image(self):
        # 使用 PyAutoGUI 截取全屏截图  
        screenshot = pyautogui.screenshot()  
  
        # 将截图转换为 numpy 数组  
        # 注意：PyAutoGUI 返回的图像是 PIL 图像对象，需要转换为 RGB 格式的 numpy 数组  
        frame = np.array(screenshot)  
  
        # 将图像从 RGB 格式转换为 BGR 格式，因为 OpenCV 使用 BGR 格式  
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)  
  
        # 返回 OpenCV 图像对象  
        return frame  

    # 获取游戏画面
    # 返回值：opencv的image对象
    def get_gamecapture_image(self, hWnd):  
        # 获取游戏窗口的位置和大小  
        left, top, width, height = self.get_game_window_rect(hWnd)  
          
        # 使用pyautogui截取屏幕指定区域  
        screenshot = pyautogui.screenshot(region=(left, top, width, height))  
          
        # 将PIL图像转换为numpy数组  
        frame = np.array(screenshot)  
          
        # 将图像从RGB转换为BGR，因为OpenCV使用BGR格式  
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)  
          
        return frame  

    # 设置鼠标位置（在整个桌面中的全局位置）  
    # 返回值：result  
    # result为True时成功，为False时失败  
    # set_cursor_pos_on_game
    def set_cursor_pos(self, x, y):  
        try:  
            pydirectinput.moveTo(int(x), int(y), duration=0)  # duration=0 表示立即移动  
            return True  
        except Exception as e:  
            print(f"设置鼠标位置失败: {e}")  
            return False  
  
    # 获取鼠标位置（在整个桌面中的全局位置）  
    # 返回值：result, x, y  
    # result为True时成功，为False时失败。result为True时x、y才有效  
    def get_cursor_pos(self):  
        try:  
            x, y = pydirectinput.position()  
            return True, x, y  
        except Exception as e:  
            print(f"获取鼠标位置失败: {e}")  
            return False, None, None  
  
    # 模拟鼠标左键按下  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_left_down(self):  
        try:  
            pydirectinput.mouseDown(button='left')  
            return True  
        except Exception as e:  
            print(f"模拟鼠标左键按下失败: {e}")  
            return False  
  
    # 模拟鼠标左键弹起  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_left_up(self):  
        try:  
            pydirectinput.mouseUp(button='left')  
            return True  
        except Exception as e:  
            print(f"模拟鼠标左键弹起失败: {e}")  
            return False  
  
    # 模拟鼠标左键点击（按下+弹起）  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_left_click(self):  
        try:  
            pydirectinput.click(button='left')  
            return True  
        except Exception as e:  
            print(f"模拟鼠标左键点击失败: {e}")  
            return False  
  
    # 模拟鼠标右键按下  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_right_down(self):  
        try:  
            pydirectinput.mouseDown(button='right')  
            return True  
        except Exception as e:  
            print(f"模拟鼠标右键按下失败: {e}")  
            return False  
  
    # 模拟鼠标右键抬起  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_right_up(self):  
        try:  
            pydirectinput.mouseUp(button='right')  
            return True  
        except Exception as e:  
            print(f"模拟鼠标右键抬起失败: {e}")  
            return False  
  
    # 模拟鼠标右键点击（按下+弹起）  
    # 返回值为True时成功，为False时失败  
    def sim_mouse_right_click(self):  
        try:  
            pydirectinput.click(button='right')  
            return True  
        except Exception as e:  
            print(f"模拟鼠标右键点击失败: {e}")  
            return False  

    # 是否锁定大写
    def is_caps_lock_on(self):
        hllDll = ctypes.WinDLL("User32.dll")
        VK_CAPITAL = 0x14
        return hllDll.GetKeyState(VK_CAPITAL) & 0xffff != 0
    
    # 锁定大小写
    # state 开关
    def set_caps_lock(self , state):
        # Get the current state of Caps Lock
        is_on = ctypes.WinDLL("User32.dll").GetKeyState(0x14) & 0xffff != 0

        # If the desired state is different from the current state, toggle Caps Lock
        if is_on != state:
            ctypes.WinDLL("User32.dll").keybd_event(0x14, 0, 0, 0)
            ctypes.WinDLL("User32.dll").keybd_event(0x14, 0, 2, 0)
        
    # 输入字符串，并模拟键盘输入  
    def input_string(self, account_string):  
        for char in account_string:
            shift_pressed = self.is_caps_lock_on()  
            if char.isupper(): # 如果字符是大写  
                if not shift_pressed: # 如果Shift键尚未按下  
                    self.set_caps_lock(True)
            elif char.islower(): # 如果字符是小写
                if shift_pressed: # 如果Shift键尚未按下  
                    self.set_caps_lock(False)
            self.sim_key_click(char.lower())
            if char.isupper(): # 如果字符是大写  
                self.sim_key_input("shift", 0) # 模拟弹起Shift键  
                shift_pressed = False  

                print(f"No VK_CODE found for character: {char}")  

    # 模拟键盘输入  
    # vkCode为按键虚拟键码，bDown=1时为按下，bDown=0时为弹起  
    # 返回值为True时成功，为False时失败  
    def sim_key_input(self, vkCode, bDown):  
        try:  
            if bDown == 1:  
                pydirectinput.keyDown(vkCode)  
            elif bDown == 0:  
                pydirectinput.keyUp(vkCode)  
            else:  
                raise ValueError("bDown 参数必须为 1 或 0")  
            return True  
        except Exception as e:  
            print(f"模拟键盘输入失败: {e}")  
            return False  
  
    # 模拟键盘点击（按下+弹起）
    # vkCode为按键虚拟键码  
    # 返回值为True时成功，为False时失败  
    def sim_key_click(self, vkCode):  
        try:  
            pydirectinput.press(vkCode)  
            return True  
        except Exception as e:  
            print(f"模拟键盘点击失败: {e}")  
            return False  

    # 模拟键盘点击（按下+弹起）
    # vkCode为按键虚拟键码  
    # 返回值为True时成功，为False时失败  
    def sim_key_click_gui(self, vkCode):  
        try:  
            pyautogui.press(vkCode)  
            return True  
        except Exception as e:  
            print(f"模拟键盘点击失败: {e}")  
            return False  
             

    # 获取字符对应的虚拟键码  
    #  
    # 参数:  
    #     char (str): 要获取虚拟键码的字符  
    #  
    # 返回值:  
    #     str: 字符对应的虚拟键码，如果未找到则返回 None  
    def get_vk_code(self, char):  
        # 使用类初始化时创建的映射表  
        return self.vk_codes.get(char.lower())  # 不区分大小写  
        
    # WindowsAPI: FindWindowW
    # className 为字符串'null'时表示传NULL,以'REGEX:'开头时启用正则表达式
    # windowName 为字符串'null'时表示传NULL,以'REGEX:'开头时启用正则表达式
    # find 是否使用匹配模式，默认为True
    # 返回值为hWnd（窗口句柄），找不到窗口时返回0
    def find_window(self, className, windowName, find=True):
         # 获取所有窗口句柄、标题和类名
        def callback(hwnd, extra):
            if win32gui.IsWindowVisible(hwnd):
                window_title = win32gui.GetWindowText(hwnd)
                window_class = win32gui.GetClassName(hwnd)
                if window_class == className:
                    extra.append((hwnd, window_title, window_class))

        windows = []
        win32gui.EnumWindows(callback, windows)
        
        for hwnd, window_title, window_class in windows:
            if not find:
                if window_title == windowName: # 全部匹配
                    return hwnd
            elif re.search(windowName, window_title): # 部分匹配
                return hwnd
        return 0


        
    # WindowsAPI: GetWindowRect 获取窗口区域
    # 第一个参数为hWnd
    # 返回值为BOOL 和 [left,top,right,bottom]
    def get_game_window_rect(self, hWnd):  
        rect = win32gui.GetWindowRect(hWnd)  
        if rect:
            return rect
        
        return None

        
    # WindowsAPI: GetClientRect 获取窗口Client区域
    # 第一个参数为hWnd
    # 返回值为BOOL,和left,top,right,bottom
    def get_client_rect(self, hWnd):  
        rect = win32gui.GetClientRect(hWnd)  
        return (True, rect) if rect else (False, (0, 0, 0, 0))  # 如果获取失败，返回False和默认的矩形

        
    # 相当于调用system(command)在Windows的cmd.exe中执行命令
    # 第一个参数为command字符串
    # 返回值为system命令的返回值（一般是命令的%ERRORLEVEL%）
    def exec_system(self, command):
        try:
            # Execute the command in cmd.exe
            result = subprocess.Popen(command)
            # Return the command's return code
            return result.returncode
        except Exception as e:
            print(f"Error executing command: {e}")
            return -1

    # 设置窗口为置顶窗口
    # 参数为：窗口句柄
    # 返回值为BOOL
    def set_window_topmost(self, hWnd):
        # return win32gui.SetForegroundWindow(hWnd)
        win32gui.SetWindowPos(hWnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) 
        win32gui.SetWindowPos(hWnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) 
        
    # 设置窗口位置
    # 参数为：窗口句柄, x, y
    # 返回值为BOOL
    def set_window_pos(self, hWnd, x, y):  
        return win32gui.SetWindowPos(hWnd, 0, x, y, 0, 0, win32con.SWP_NOSIZE)  


    # 设置窗口大小
    # 参数为：窗口句柄, w, h
    # 返回值为BOOL
    def set_window_size(self, hWnd, w, h):  
        return win32gui.SetWindowPos(hWnd, 0, 0, 0, w, h, win32con.SWP_NOMOVE)  


    # 设置窗口位置和大小
    # 参数为：窗口句柄, x,y, w, h
    # 返回值为BOOL
    def set_window_pos_size(self, hWnd, x, y, w, h):  
        return win32gui.SetWindowPos(hWnd, 0, x, y, w, h, 0)  


    #判断窗口句柄是否有效
    #需要30.2.0-rc15
    def is_window(self, hWnd):
        return win32gui.IsWindow(hWnd)  
        
    #判断窗口是否可见
    #需要30.2.0-rc15
    def is_window_visible(self, hWnd):
        return win32gui.IsWindowVisible(hWnd)  
    
    #判断窗口是否卡死
    #需要30.2.0-rc11
    def is_window_ghosted(self, hWnd):
        pass
    
    #获取窗口所属PID
    #需要30.2.0-rc11
    def get_window_process(self, hWnd):  
        # 获取窗口所属的线程ID  
        thread_id, process_id = win32process.GetWindowThreadProcessId(hWnd)  
        return process_id  

    
    #强制结束进程
    #需要30.2.0-rc11
    def terminate_process(self, pid):  
        handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE, False, pid)  
        win32api.TerminateProcess(handle, 0)  
        win32api.CloseHandle(handle)  
    
    #根据进程名查找进程id
    #找不到时返回0
    #需要30.2.0-rc13
    def find_process_id_by_name(self, name):  
        # 遍历所有进程，查找与名称匹配的进程ID  
        for proc in psutil.process_iter(['pid', 'name']):  
            if proc.info['name'] == name:  
                return proc.info['pid']  
        return 0  

    
    # 相当于调用ShellExecuteW
    # param和curdir可以为'null'
    # 返回值为BOOL
    #需要30.2.0-rc16
    def shell_execute(self, command, param=None, curdir=None):  
        # 使用subprocess库执行shell命令  
        try:  
            if param:  
                command = f"{command} {param}"  
            if curdir:  
                subprocess.run(command, shell=True, cwd=curdir, check=True)  
            else:  
                subprocess.run(command, shell=True, check=True)  
            return 1  # 成功  
        except subprocess.CalledProcessError:  
            return 0  # 失败  


    # 切换键盘布局
    # layout: 
    # 美式英语："00000409"
    # 英国英语："00000809"
    # 中文（简体）："00000804"
    # 中文（繁体）："00000404"
    # 返回值为BOOL：0 / 1
    # 需要30.2.0-rc17
    def switch_keyboard_layout(self, layout):  
        # 使用win32api切换键盘布局  
        try:    
            # ctypes.windll.user32.LoadKeyboardLayoutW
            new_layout = win32api.LoadKeyboardLayout(layout, win32con.KLF_ACTIVATE)
            print(new_layout)
    
            # LoadKeyboardLayout = ctypes.windll.user32.LoadKeyboardLayoutW  
            # LoadKeyboardLayout(layout, wintypes.KLF_ACTIVATE)  
            return 1  # 成功  
        except Exception as e:  
            # 可以打印异常信息或记录日志
            print(e)
            return 0  # 失败  

        
    # 切换目标窗口的键盘布局
    # hWnd: 窗口句柄
    # layout: 
    # 美式英语："00000409"
    # 英国英语："00000809"
    # 中文（简体）："00000804"
    # 中文（繁体）："00000404"
    # 返回值为BOOL：0 / 1
    # 需要30.2.0-rc19
    def switch_keyboard_layout_for_window(self, hWnd, layout): 
        user32 = ctypes.WinDLL('user32', use_last_error=True) 
        # Load the Layout
        hkl = user32.LoadKeyboardLayoutW(layout, 1)
        if not hkl:
            print("Failed to load keyboard layout.")
            return 0

        # Activate the Layout for the given window
        result = user32.SendMessageW(hWnd, 0x0050, 0, hkl)
        if not result:
            print("Failed to switch keyboard layout.")
            return 0

        return 1


    # 等价于：SendMessageA(hWnd, Msg, wParam, lParam)
    # 需要30.2.0-rc18
    def send_message_to_window(self, hWnd, Msg, wParam, lParam):  
        # 使用win32api发送消息到窗口  
        try:  
            result = win32api.SendMessage(hWnd, Msg, wParam, lParam)  
            return result  
        except Exception as e:  
            # 可以打印异常信息或记录日志  
            return 0  # 失败  

    def find_image_in_screenshot(self, large_image, small_image_path , Sim):
        """
        在当前截图中查找小图的位置及其窗口矩形。

        Args:
            small_image_path (str): 小图的文件路径。

        Returns:
            tuple or None: 包含小图在截图中位置的左上角坐标(x, y)和窗口矩形(x, y, width, height)的元组，如果未找到或发生错误则返回None。
        """
        try:
            # 检查小图是否已经在缓存中
            if small_image_path in self.small_image_cache:
                small_image = self.small_image_cache[small_image_path]
            else:
                # 读取小图并存储到缓存中
                small_image = cv2.imread(small_image_path)  # 以彩色模式读取小图
                if small_image is None:
                    raise ValueError("无法加载小图图片")
                self.small_image_cache[small_image_path] = small_image

            # 使用matchTemplate寻找小图在大图中的位置
            result = cv2.matchTemplate(large_image, small_image, cv2.TM_CCOEFF_NORMED)
            threshold = 0.8  # 设置匹配阈值
            if Sim :
                threshold = Sim  # 设置匹配阈值
                
            loc = np.where(result >= threshold)

            # 如果没有找到匹配项,则直接返回None
            if not loc[0].size:
                return None

            # 找到最佳匹配位置(即result中的最大值位置)
            min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

            # 提取最佳匹配的坐标
            pt = (max_loc[0], max_loc[1])

            # 计算小图在大图中的窗口矩形
            small_image_height, small_image_width, _ = small_image.shape
            window_rect = (pt[0], pt[1], small_image_width, small_image_height)

            return (window_rect)

        except Exception as e:
            print(f"在查找小图时发生错误: {str(e)}")
            return None


# if  __name__ == "__main__":
#     api=OBSApi()
#     time.sleep(3)
    # api.sim_key_click("enter")
    # api.sim_key_click("enter")
    # api.sim_key_click("pageup")
    # print(11111)
#     pyperclip.copy("WangQuan001")
#     pyautogui.hotkey('ctrl', 'v')
#     hwnd = api.find_window("Notepad", "无标题 - 记事本")
#     print(api.switch_keyboard_layout("00000409"))
#     api.input_string("WangQuan001")
#     # while True:
#     #     time.sleep(1)
#     #     pydirectinput.keyDown('shift')  
  
#     #     # 等待一段时间，这里设置为2秒，可以根据需要调整  
#     #     time.sleep(0.5)  
        
#     #     # 释放 'shift' 键  
#     #     pydirectinput.keyUp('shift')

